<?php
/**
 * REKS framework is a very lightweight and small footprint PHP 5.3+ Framework.
 * It supports a limited set of features but fully MVC based and Objectoriented.
 * 
 * Copyright (c) 2012, REKS group ( Lars Martin Rørtveit, Andreas Elvatun, Petter Kjelkenes, Pål André Sundt )
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the REKS GROUP nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL "REKS Group" BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @license 3-clause BSD
 * @package reks
 * @author REKS group at Telemark University College
 */
namespace reks;



/**
 * Router implementation.
 * Very simple router that allows routing of urls to use a specific controller / method.
 * It also supports dynamic arguments.
 * 
 * Router is created with the State Machine principle.
 * See constants for states.
 *
 * @author REKS group at Telemark University College
 * @version 1.0
 *
 */
class Router{
	/**
	 * Array of configuration from config.php
	 * @var array
	 */
	private $config = array();


	/**
	 * The starting time of the constructor of the Router in float microtime.
	 * @var float
	 */
	private $startTime;


	/**
	 * Logger instance. Used to log application data.
	 * @var reks\Log
	 */
	public $log;

	
	/**
	 * State: Ordinary - initial state.
	 * @var int
	 */
	const S_ORD = 1;
	/**
	 * State: Variable - when variable @ sign found
	 * @var int
	 */
	const S_VAR = 2;
	/**
	 * State: Regexp - Regexp state, when in a regexp < .. >
	 * @var int
	 */
	const S_IN_REG = 3;
	/**
	 * State: Method - When method is opened.
	 * @var int
	 */
	const S_METHOD_O = 4;
	/**
	 * State: Arguments - When in arguments.
	 * @var int
	 */
	const S_ARGS = 5;
	
	/**
	 * Array of routes.
	 * @var array
	 */
	private $routes = array();
	
	/**
	 * Path info reference for the URL.
	 * This should be "/" for main page ( no arguments in the url )
	 * Ususally $_SERVER['PATH_INFO'] is just correct to pass into this variable.
	 * @var string
	 */
	private $uri;
	
	
	/**
	 * Just string of alpha characters a-zA-Z
	 * @var string
	 */
	const alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
	/**
	 * String of numeric chars 0-9
	 * @var string
	 */
	const numeric = '0123456789';
	
	// These variables helps a lot for performance.
	private $vParseCache = array();
	private $componentsCache = array();
	private $reverseCache = array();
	
	/**
	 * Constructs the router values.
	 */
	public function __construct($startTime, array $config, Log $log, $pathInfo){
		$this->startTime = $startTime;
		$this->config = $config;
		$this->log = $log;
		$this->uri = $pathInfo;
		$this->routes = $config['route'];
		
		
		$this->log->debug("\n--APP & LOGGER STARTED--");
	}
	
	/**
	 * Sets the uri.
	 * @param string $uri Input URI ( eg. / or /test )
	 */
	public function setURI($uri){
		$this->uri = $uri;
	}

	
	/**
	 * Reverses a route by controller / method and arguments.
	 * If this method does not find a method - exception will be thrown.
	 * 
	 * Focusing on holding this algorithm fast is a must.
	 * 
	 * @param string $path The path ( controller.method )
	 * @param array $args Arguments, keys must be the same name as in the routes file.
	 * @param string $requestType get, post or  * / null if you want to look for all.
	 * @throws \Exception Exception is thrown if no routes is found.
	 */
	public function reverse($path, $args=array(), $requestType = null){
		// If no path, well return nothing.
		if (!$path) return;
		
		$countArgs = count($args);
		
		// We could also implode array_keys of $args, but that takes to much time, enough with values 
		// Wich are indeed the best way to go.
		$hash = $path.implode('', $args);
		// Allow cached version to skip all the parsing.
		if (isset($this->reverseCache[$hash])) return $this->reverseCache[$hash];
		
		
		// Count up $path var.
		$cnt = strlen($path);
		// Get method + controller from $path input.
		$e = explode('.', $path);
		
		list ($input_co, $input_me) = $e;
		
		// Loop all types.
		foreach($this->routes as $type => $types){
			if ($requestType !== null && $requestType != '*' && $type != $requestType)continue;
			// Loop all routes.
			foreach($types as $from => $to){

				
				// Get components.
				list ($controller, $method, $arguments) = $this->getComponents($to);
				
				// Check if this is dynamic controller / Method. In this case - some more resources will be used.
				$earlyComponentGet = false;
				if (substr($controller, 0, 1) == '@' || substr($method, 0, 1) == '@'){
					$fakeArgs = array();
					list($from, $vars) = $this->vParser($from);
					// Rewrite controller / method dynamic parts to real parts.
					foreach($vars as $set){
						if ($set['var'] == $controller){
							$controller = $input_co;
							$fakeArgs[substr($set['var'],1)] = $controller;
						}else if ($set['var'] == $method){
							$method = $input_me;
							$fakeArgs[substr($set['var'],1)] = $method;
						}
					}
					
					$earlyComponentGet = true;
				}
				
				
				// If We get example ways News.index , from $path , its all ok to now.
				// This check before $this->getComponents is 2x faster.
				if ($input_co == $controller && $input_me == $method){
					
					
					
					// If amount of args is equal.
					if ($countArgs == count($arguments)){
						
						// No arguments. No parsing is done.
						if ($countArgs == 0 && !$earlyComponentGet)return $from;
						
						$argsOk = true;
						// Check if all argument keys matches the real onces.
						foreach($args as $k => $v){
							if (!in_array('@'.$k, $arguments))$argsOk=false;
						}
						
						// Now ... lets get get a script smile - sorry in a good mood.
						// Lets make some check to add fake arguments.
						if ($earlyComponentGet){
							$args = array_merge($args, $fakeArgs);
						}
						
						// All done, lets reroute.
						if ($argsOk){
							// Parse from field.
							if (!$earlyComponentGet)list($from, $vars) = $this->vParser($from);
							
							
							// Explode into bits , separated by "/".
							$bits = explode('/', $from);
							
							// We can still stop this loop if the arguments does not match data type.
							$dataTypeError = false;
							
							$j = 0;
							// Loop each bit.
							foreach($bits as $k => $bit){
								
									
								// If we since start of regexp pattern.
								if (substr($bit, 0, 1) == '('){
									// Change the bit to the real value
									// Escape @ from vars and bind $j from $args
									$realval = $args[substr($vars[$j]['var'], 1)];
									
									// Check if datatype is correct.
									if (preg_match("/^$bit\$/", $realval)){
										$bits[$k] = $realval;
									// Datatype is not correct, lets go all the way to next check.
									}else{
										$dataTypeError = true;
									}
									$j++;
								}
							}
							if (!$dataTypeError){
								// Implode again.
								$ret =  implode('/', $bits);
								$this->reverseCache[$hash] = $ret;
							
								return $this->reverseCache[$hash];
							}
						}
					}
				}
			}
		}
	
		throw new RouterReverseException("Could not reverse route $path with arguments ".implode(', ', $args).". No applicable route found for this controller/method combination.");
	}
	
	
	/**
	 * Tries to parse the routes.
	 */
	public function route(){
		session_start();
		
		try{
			if ($components = $this->dispatch()){
				list ($controller, $method, $args) = $components;
				try{
					$this->load($controller, $method, $args);
				}catch(InternalServerError $e){
					
					$c = $this->trigger('500', null, array('message' => $e->getMessage()), 500);
				}
			}else {
				$c = $this->trigger('404', null, array(), 404);
			}
		}catch(\Exception $e){
			$this->log->error($e->getMessage());
				
			echo "
			<h1>".get_class($e)."</h1>
			<p><strong>Message:</strong><br />{$e->getMessage()}</p>
			<p><strong>Stack:</strong><br />{$e->getTraceAsString()}</p>
			";
			
		}
	
			
	}
	
	private function dispatch(){
		// Make them to lower.
		$requestType = strtolower($_SERVER['REQUEST_METHOD']);
		
		// Now, don't loop those requests that are not really needed.
		
		// If we have some routes in this category.
		if (isset($this->routes[$requestType])){
			// Loop those routes.
			foreach($this->routes[$requestType] as $from => $to){
				if ($r = $this->compareFrom($from, $to))return $r;
			}
		}else if(isset($this->routes['*'])){
			// Loop * routes.
			foreach($this->routes['*'] as $from => $to){
				if ($r = $this->compareFrom($from, $to))return $r;
			}
		}
		return null;
	}
	
	/**
	 * Parses a from route to application array of variables.
	 * Returns a parsed regexp from route and array of variables with names only.
	 * @param string $from
	 */
	public function vParser($from){
		// Enable static caching.
		// This only needs to be run once.
		if (isset($this->vParseCache[$from])){
			return $this->vParseCache[$from];
		}
		
		$bits = str_split($from);
		$state = self::S_ORD;
	
		$vars = array();
		$vK = -1;
		// Var length of a variable. 
		$varLen = 0;
		
		
		// Tested - Roughly 3 x Faster then regexp.
		
		$varPattern = self::alpha."._";
		$ordPattern = self::numeric.self::alpha."/_-.";
		
		foreach($bits as $k => $c){
			switch($state){
				case self::S_ORD:
					// Start of var.
					if ($c == '@'){
						// Reset var length.
						$varLen = 0;
						// Increase variable iterator.
						$vK++;
						
						$state = self::S_VAR;
						
						// Avoid stupid warnings.
						if (!isset($vars[$vK])){
							$vars[$vK] = null;
						}
						if (!isset($vars[$vK]['var'])){
							$vars[$vK]['var'] = null;
						}
						
						$vars[$vK]['var'] .= $c;
					// Pass ...
					}elseif(strstr($ordPattern, $c)){
						
					// Do not pass. Error.
					}else{
						throw new RouteParseException("Unexpected character: '$c'  in FROM part route $from.", "@$ordPattern");
					}
					break;
				case self::S_VAR:
					// Increase var length.
					$varLen++;
					// End of var, remove semicolon.
					if($c == ';' && $varLen > 1){
						unset($bits[$k]);
						$state = self::S_ORD;
					// Also end of var, but keep the slash.
					}elseif($c == '/' && $varLen > 1){
						$state = self::S_ORD;
					// Start regexp
					}elseif($c == '<' && $varLen > 1){
						unset($bits[$k]);
						$state = self::S_IN_REG;
					// Append to var
					}elseif (strstr($varPattern, $c)){
						unset($bits[$k]);
						$vars[$vK]['var'] .= $c;
					// Parser error.
					}else{
						throw new RouteParseException("Unexpected character: '$c'  in FROM part (Variable) of route $from.", ($varLen > 1 ? ';/<' : '').$varPattern);
					}
					break;
				case self::S_IN_REG:
					if ($c == '>'){
						unset($bits[$k]);
						$state = self::S_ORD;
					// Allow all characters here.
					}else{
						unset($bits[$k]);
						if (!isset($vars[$vK]['regexp'])) $vars[$vK]['regexp'] = null;
						$vars[$vK]['regexp'] .= $c;
					}
					break;
			}
		}
		
		
	
		$j = 0;
		foreach($bits as $k => $v){
			if ($v == '@'){
				$var = $vars[$j];
				$bits[$k] = isset($var['regexp']) ? '('.$var['regexp'].')' : '([A-Za-z0-9_\.\-]*)';
				$j++;
			}
		}
		
	
		$ret = array(
				implode('',$bits),
				$vars);
		
		$this->vParseCache[$from] = $ret;
		
		return $this->vParseCache[$from];
	}
	
	/**
	 * Gets Controller, method and arguments from a $to string.
	 * @param string $to A to string.
	 */
	public function getComponents($to){
		if (isset($this->componentsCache[$to]))return $this->componentsCache[$to];
		$controller = '';
		$method = '';
		$args = array();
	
		$state = self::S_ORD;
	
		$bits = str_split($to);
	
		
		$toVarPattern = self::alpha.self::numeric.'_@';
		$mPattern = $toVarPattern;
		$cPattern = "$toVarPattern/";
		
		// Argument key.
		$aK = 0;
	
		foreach ($bits as $c){
			switch($state){
				case self::S_ORD:
					if ($c == '.'){
						$state = self::S_METHOD_O;
					}elseif(strstr($cPattern, $c)){
						$controller .= $c;
					}else{
						throw new RouteParseException("Unexpected character: $c in TO part (Controller) of route $to.", ".$cPattern");
					}
					break;
				case self::S_METHOD_O:
					if ($c == '('){
						$state = self::S_ARGS;
					}elseif(strstr($mPattern, $c)){
						$method .= $c;
					}else{
						throw new RouteParseException("Unexpected character: $c in TO part (Method) of route $to.", "($mPattern");
					}
					break;
	
				case self::S_ARGS:
					if ($c == ','){
						$aK++;
					}elseif($c == ')'){
						$state = self::S_ORD;
					}elseif(strstr($toVarPattern, $c)){
						if (!isset($args[$aK])) $args[$aK] = null;
						$args[$aK] .= $c;
					}else if ($c == ' ' || $c == "\t" || $c == "\n" || $c == "\r"){
						// Just skip, OK.
					}else{
						throw new RouteParseException("Unexpected character: '$c' in TO part (Arguments) of route $to.", ",) $toVarPattern");
					}
					break;
	
			}
		}
	
		$ret =  array(
				$controller,
				$method,
				$args
		);
		
		$this->componentsCache[$to] = $ret;
		
		return $this->componentsCache[$to];
	}
	
	/**
	 * Gets a finished parsed components from a $to and $vars input.
	 * $vars are from $this->vParser().
	 * @param string $to 
	 * @param array $vars
	 */
	public function getBackend($to, $vars){
	
		list ($controller, $method, $args) = $this->getComponents($to);
	
	
		// And finally find the correct variable and pass value to the argument key.
		foreach($args as $k => $v){
			foreach($vars as $j => $var){
				if ($var['var'] == $v){
					$args[$k] = $var['val'];
					// Dont need this anymore.
					unset($vars[$j]);
				}
			}
		}
		
		// Allows for integration against parameters.
		foreach($vars as $k => $var){
			if (substr($var['var'], 0, 2) == '@.'){
				$exp = explode('.',substr($var['var'],2));
				
				list($t, $n) = $exp;
				switch(strtolower($t)){
					case 'get':
						$_GET[$n] = $var['val'];
						break;
					case 'post':
						$_POST[$n] = $var['val'];
						break;
					case 'session':
						$_SESSION[$n] = $var['val'];
						break;
				}
				unset($vars[$k]);
			}
		}
	
		// Now, for rest of the vars... replace.
		// Rest of the vars will be replaced within the controller or method scope.
		foreach($vars as $j => $var){
			if (strstr($controller.$method, $var['var'])){
				$controller = str_replace($var['var'], $var['val'], $controller);
				$method = str_replace($var['var'], $var['val'], $method);
				unset($vars[$j]);
			}
		}
	
	
		return array($controller,
				$method,
				$args);
	}
	
	
	/**
	 * Compares a from and returns the components if $from matches.
	 * @param string $from
	 * @param string $to
	 */
	public function compareFrom($from, $to){
	
		
		list($from, $vars) = $this->vParser($from);
		
		if (preg_match("#^{$from}$#", $this->uri, $matches)){
			
			foreach($matches as $k => $match){
				if ($k != 0)$vars[$k-1]['val'] = $match;
			}
			return $this->getBackend($to, $vars);
		}
		return null;
	}
		

	/**
	 * Triggers a specific router rule based on the router rule name.
	 *
	 * @param string $key The key of the router rule.
	 * @param array $arguments Arguments that should be passed to the method.
	 * @return reks\Controller
	 * @throws reks\InternalServerError Throw exception if we could not trigger the route.
	 */
	public function trigger($key, array $arguments = null, array $viewVars = array(), $statusCode = null){
		foreach($this->routes as $types){
			foreach($types as $from => $to){
				if ($from==$key){
					list($controller, $method, $args) = $this->getComponents($to);
					
					return $this->load($controller, $method, $arguments, $viewVars, $statusCode);
				}
			}
		}
		throw new InternalServerError("Could not load route $key. ");
		
	}


	/**
	 * This method should be run by the router itself.
	 *
	 *
	 * @param string $controller Controller name.
	 * @param string $method Method name.
	 * @param array $args Arguments that should be passed into the Method.
	 * @param array $assignVars View variables to be passed.
	 * @param int $statusCode What HTTP code do you want to use ?
	 * @return reks\Controller A controller object.
	 */
	protected function load($controller, $method, $args=null, array $assignVars=array(), $statusCode = null){
		// Conform to php NS syntax.
		$controller = str_replace('/', '\\', $controller);
		
		// Add namespace.
		if (substr($controller, 0, 1) != '\\'){
			$controller = '\controller\\' . $controller;
		}
		
		if (!class_exists($controller))throw new InternalServerError("Could not find class {$controller} because its not really there..");		
		if (!method_exists($controller, $method))throw new InternalServerError("Could not find METHOD {$method} in class {$controller}.");

		// Components....
		$ui = new Userinput();
		$csrf = new Csrf($ui);
		$url = new Url($this, $this->config['remove_scriptpath'], $csrf->token('safe_link_csrf'));
		$state = new State($url);
		$lang = new Lang(APP_PATH . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . $this->config['language'] . '.php');
		
		$view = new View($this->config, $lang, $url);
		$request = new Request();
		$modelWrapper = new ModelWrapper($this->config, $lang, $this->log);
		
		$c = Controller::init(
			$this, 
			$this->config, 
			$controller, 
			$url, 
			$state, 
			$lang, 
			$ui, 
			$csrf, 
			$view, 
			$request, 
			$this->log, 
			$modelWrapper,
			new ActiveRoute($controller, $method, $args)
		);
				
		
		if ($statusCode){
			$c->sendStatus($statusCode);
		}
		
		$this->log->debug("Loaded $controller::$method");
		
		foreach($assignVars as $key => $val){
			$c->view->assign($key, $val);
		}
		
		// Hvis vi har argumenter.
		if (is_array($args) && count($args) > 0) call_user_func_array(array($c, $method), $args);
		// Hvis vi ikke har argumenter.
		else $c->$method();

		return $c;
	}

	/**
	 * Tests all the routes.
	 * 
	 * @throws RouteParseException Throws RouteParseException on failure to compile a specific route.
	 */
	public function testRoutes(){
		foreach($this->routes as $types){
			foreach($types as $from => $to){
				list($from, $vars) = $this->vParser($from);
				foreach($vars as $k => $v){
					$vars[$k]['val'] = 0;
				}
				$this->getBackend($to, $vars);
			}
		}
	}
	
	

	/**
	 * Gets execution time at this time.
	 */
	public function getExecutionTime(){
		return microtime(true) - $this->startTime;
	}

}

/**
 * InternalServerError is a custom Exception to be casted when there are errors that should stop all code and stop execution.
 * When this Exception is casted from any controller user will get a 500 http response and a custom 500 page can be set.
 *
 * @author REKS group at Telemark University College
 *
 */
class InternalServerError extends \Exception{

}
class RouterReverseException extends \Exception{
	
}
class RouteParseException extends \Exception{
	public function __construct($message, $expectedSymbols=null){
		if ($expectedSymbols !== null){
			$message .= "Expected One Of $expectedSymbols";
		}
		parent::__construct("Router parse exception ".$message, 1, null);
	}
}

/**
 * Class to return active router pair combination.
 * @author loltroll
 *
 */
class ActiveRoute{
	private $args;
	private $controller;
	private $method;
	
	/**
	 * 
	 * @param string $controller Controller name.
	 * @param string $method Method name
	 * @param mixed $args Null or array of args.
	 */
	public function __construct($controller, $method, $args){
		$this->args = $args;
		$this->controller = $controller;
		$this->method = $method;
	}
	/**
	 * Fetches array or NULL of arguments passed.
	 */
	public function getArgs(){
		return $this->args;
	}
	/**
	 * Fetches the controller name of the active real controller.
	 */
	public function getController(){
		return $this->controller;
	}
	/**
	 * Fetches the method name of the active real controller/method.
	 */
	public function getMethod(){
		return $this->method;
	}
	
	
}